﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unikado.PHPLibs.Tokenizer.Types;

namespace Unikado.PHPLibs.Tokenizer
{

    /// <summary>
    /// This is a collection of PHP Tokens.
    /// </summary>
    public class TokenCollection : IEnumerable<Token>
    {

        #region - - - - - -   P R I V A T E   F I E L D S   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        private readonly List<Token> data;

        #endregion

        #region - - - - - -   P U B L I C   P R O P E R T I E S   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// Returns the token for defined index.
        /// </summary>
        /// <param name="index">The index (0-n)</param>
        /// <returns>Token</returns>
        public Token this[int index] { get { return data[index]; } }
        /// <summary>
        /// Returns the last defined token, or <c>null</c> if no token is contained.
        /// </summary>
        public Token LastToken
        {
            get
            {
                if (data.Count < 1) return null;
                return data[data.Count - 1];
            }
        }
        /// <summary>
        /// Returns, how many tokens are currently defined.
        /// </summary>
        public int Count { get { return data.Count; } }

        #endregion

        #region - - - - - -   C O N S T R U C T O R S   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// Inits a new instance.
        /// </summary>
        public TokenCollection()
        {
            data = new List<Token>();
        }

        #endregion

        #region - - - - - -   I N T E R N A L   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        internal Token Add(Token token)
        {
            token.Index = data.Count;
            data.Add(token);
            return data[token.Index];
        }
        internal Token AddNoPhp(string code, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.None, TypeNormal.None, TypeFine.None, "", lnum, blev, clev, slev));
        }
        internal Token AddBracket(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Bracket, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddComment(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Comment, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddComment(string code, int lnum, int blev, int clev, int slev)
        {
            if ('#' == code[0])
                return Add(new Token(code, TypeRough.Comment, TypeNormal.Comment, TypeFine.HashComment, "", lnum, blev, clev, slev));
            if (code.Length > 5 && code.StartsWith("/**"))
                return Add(new Token(code, TypeRough.Comment, TypeNormal.Docblock, TypeFine.Docblock, "", lnum, blev, clev, slev));
            if (code.Length > 5 && code.StartsWith("/*"))
                return Add(new Token(code, TypeRough.Comment, TypeNormal.Comment, TypeFine.MultilineComment, "", lnum, blev, clev, slev));
            return Add(new Token(code, TypeRough.Comment, TypeNormal.Comment, TypeFine.SlashComment, "", lnum, blev, clev, slev));
        }
        internal Token AddInclude(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Include, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddKeyword(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Keyword, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddKeywordSignature(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.KeywordSignature, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddNumber(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Number, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddInteger(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Number, TypeNormal.Integer, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddFloat(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Number, TypeNormal.Float, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddOperator(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddAritmethicOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.AritmethicOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddAssignmentOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.AssignmentOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddComparisonOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.ComparisonOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddLogicalOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.LogicalOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddOtherOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.OtherOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddScopeOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.ScopeOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddXCrementOperator(string code, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Operator, TypeNormal.XCrementOperator, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddOther(string code, TypeNormal ntype, TypeFine ftype, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.Other, ntype, ftype, "", lnum, blev, clev, slev));
        }
        internal Token AddPhpOpenTag(string code, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.PhpTag, TypeNormal.PhpTag, TypeFine.PhpTagOpen, "", lnum, blev, clev, slev));
        }
        internal Token AddPhpCloseTag(string code, int lnum, int blev, int clev, int slev)
        {
            return Add(new Token(code, TypeRough.PhpTag, TypeNormal.PhpTag, TypeFine.PhpTagClose, "", lnum, blev, clev, slev));
        }
        internal Token AddString(string code, int lnum, int blev, int clev, int slev)
        {
            if ('\'' == code[0])
                return Add(new Token(code, TypeRough.String, TypeNormal.String, TypeFine.StringSingle, "", lnum, blev, clev, slev));
            if ('"' == code[0])
                return Add(new Token(code, TypeRough.String, TypeNormal.String, TypeFine.StringDouble, "", lnum, blev, clev, slev));
            if ('`' == code[0])
                return Add(new Token(code, TypeRough.String, TypeNormal.Command, TypeFine.Command, "", lnum, blev, clev, slev));
            if ('\'' == code[3])
                return Add(new Token(code, TypeRough.String, TypeNormal.HereNowDoc, TypeFine.NowDoc, "", lnum, blev, clev, slev));
            return Add(new Token(code, TypeRough.String, TypeNormal.HereNowDoc, TypeFine.HereDoc, "", lnum, blev, clev, slev));
        }
        internal void AddSpace(string code)
        {
            LastToken.SpaceAfter += code;
        }
        internal void AddSpace(char c)
        {
            LastToken.SpaceAfter += c;
        }
        internal Token AddLineBreak(int lnum, int blev, int clev, int slev)
        {
            return Add(new Token("\n", TypeRough.Whitespace, TypeNormal.NewLine, TypeFine.NewLine, "", lnum, blev, clev, slev));
        }

        #endregion

        #region - - - - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// …
        /// </summary>
        /// <returns>…</returns>
        public IEnumerator<Token> GetEnumerator()
        {
            return ((IEnumerable<Token>) data).GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
        /// <summary>
        /// Removes all defined tokens (cleans the collection)
        /// </summary>
        public void Clear()
        {
            data.Clear();
        }
        /// <summary>
        /// Returns the complete contained Code.
        /// </summary>
        /// <returns>All defined PHP source</returns>
        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var token in data) sb.Append(token);
            return sb.ToString();
        }
        /// <summary>
        /// Inserts a new DocBlock at defined index. At least it will be inserted at index + 1
        /// because before the BocBlock a line break + required spaces are inserted.
        /// </summary>
        /// <param name="index">The index to inset Linebrek + DocBlock.</param>
        /// <param name="docBlock">The ready formatted Docblock. Only the line incrementation is adjusted by this method.</param>
        /// <param name="tabWidth">How many spaces represents a tabulator?</param>
        /// <returns>The new DocBlock Token is returned</returns>
        /// <exception cref="ArgumentOutOfRangeException">If index is out of the allowed range</exception>
        /// <exception cref="ArgumentException">If Index points to a other DocBlock element</exception>
        public Token InsertDocBlock(int index, string docBlock, int tabWidth = 4)
        {
            if (index < 0 || index > Count) throw new ArgumentOutOfRangeException("index",
                @"Index " + index + @" for a new Docblock is out of the usablable range (0-" + Count);
            if (data[index].NormalType == TypeNormal.Docblock) throw new ArgumentException(
                "Can not add a docblock token for index " + index + " if the current token of this index is also a docblock!");
            var incChars0 = new string(' ', tabWidth * data[index].Blocklevel);
            var incChars1 = incChars0 + ' ';
            data.Insert(index, new Token("\n", TypeRough.Whitespace, TypeNormal.NewLine, TypeFine.NewLine, incChars0, data[index].LineNumber,
                data[index].Blocklevel, data[index].Conditionlevel, data[index].Squarelevel) {Index = index});
            string[] lines = docBlock.Replace("\r\n", "\n").Split('\n');
            for (int i = 1; i < lines.Length; i++) { lines[i] = incChars1 + lines[i].Trim(); }
            index++;
            int lnum = data[index].LineNumber + 1;
            data.Insert(index, new Token(string.Join("\n", lines), TypeRough.Comment, TypeNormal.Docblock, TypeFine.Docblock, "", lnum,
                data[index].Blocklevel, data[index].Conditionlevel, data[index].Squarelevel) { Index = index });
            lnum += lines.Length - 1;
            int idx = index;
            index++;
            for (; index < Count; index++)
            {
                data[index].Index = index;
                data[index].LineNumber = lnum;
                if (data[index].NormalType == TypeNormal.NewLine) lnum++;
                else lnum += data[index].LinebreakCount;
            }
            return data[idx];
        }
        /// <summary>
        /// Updates the contents of a existing Docblock.
        /// </summary>
        /// <param name="index">The index of the existing docblock</param>
        /// <param name="docBlock">The ready formatted Docblock. Only the line incrementation is adjusted by this method.</param>
        /// <param name="tabWidth">How many spaces represents a tabulator?</param>
        /// <returns>The affected DocBlock Token is returned</returns>
        /// <exception cref="ArgumentOutOfRangeException">If index is out of the allowed range</exception>
        /// <exception cref="ArgumentException">If Index to not point to a DocBlock element</exception>
        public Token UpdateDocBlock(int index, string docBlock, int tabWidth = 4)
        {
            if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index",
                @"Index " + index + @" for a Docblock update is out of the usablable range (0-" + (Count - 1));
            if (data[index].NormalType != TypeNormal.Docblock) throw new ArgumentException(
                "Can not update the token with index " + index + " because its not a docblock token!");
            var incChars0 = new string(' ', tabWidth * data[index].Blocklevel);
            var incChars1 = incChars0 + ' ';
            string[] lines = docBlock.Replace("\r\n", "\n").Split('\n');
            for (int i = 1; i < lines.Length; i++) { lines[i] = incChars1 + lines[i].Trim(); }
            data[index].Code = string.Join("\n", lines);
            int lnum = data[index].LineNumber + data[index].LinebreakCount;
            for (int i = index + 1; i < Count; i++)
            {
                data[i].Index = index;
                data[i].LineNumber = lnum;
                if (data[index].NormalType == TypeNormal.NewLine) lnum++;
                else lnum += data[index].LinebreakCount;
            }
            return data[index];
        }
        /// <summary>
        /// Returns a enumerator over all current defined declarations. A declaration ist allready the
        /// code that defines a new Class, interface, method, function, constant, class constant,
        /// class field/property, namespace or use. For every declaration is returned the index of the first
        /// declaration token, and the index of the last declaration tokaen, as a <c>int[2]</c> Array.
        /// </summary>
        /// <returns>List of declaration start- and end token indexes.</returns>
        public List<int[]> GetIndexesOfAllDeclarations()
        {
            bool isPhp = false;
            bool search = false;
            var returnValue = new List<int[]>();
            for (int i = 0; i < Count; i++)
            {
                if (!isPhp)
                {
                    if (data[i].FineType == TypeFine.PhpTagOpen) isPhp = true;
                    continue;
                }
                if (data[i].FineType == TypeFine.PhpTagClose)
                {
                    isPhp = false;
                    continue;
                }
                if (!search)
                {
                    if (data[i].NormalType == TypeNormal.NewLine) search = true;
                    continue;
                }
                if (data[i].NormalType == TypeNormal.NewLine) continue;
                search = false;
                if (data[i].RoughType != TypeRough.KeywordSignature) continue;
                if (i + 1 >= Count) return returnValue;
                int[] result = { i, i + 1 };
                switch (data[i].FineType)
                {
                    case TypeFine.Abstract:
                        #region Abstract
                        if (data[i + 1].FineType == TypeFine.Class)
                        {
                            for (; i < Count; i++)
                            {
                                if (data[i].FineType != TypeFine.BlockOpenBracket) continue;
                                result[1] = i - 1;
                                if (data[i - 1].NormalType == TypeNormal.NewLine) result[1] = i - 2;
                                if (data[result[1]].RoughType == TypeRough.Comment) result[1]--;
                                if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                                returnValue.Add(result);
                                break;
                            }
                            break;
                        }
                        for (; i < Count; i++)
                        {
                            if (data[i].FineType != TypeFine.Semicolon) continue;
                            result[1] = i;
                            if (data[i].NormalType == TypeNormal.NewLine) result[1] = i - 1;
                            if (data[result[1]].RoughType == TypeRough.Comment) result[1]--;
                            if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                            returnValue.Add(result);
                            break;
                        }
                        break;
                        #endregion
                    case TypeFine.Class:
                    case TypeFine.Interface:
                    case TypeFine.Trait:
                    case TypeFine.Final:
                        #region Class, Interface, Trait, Final
                        for (; i < Count; i++)
                        {
                            if (data[i].FineType != TypeFine.BlockOpenBracket) continue;
                            result[1] = i - 1;
                            if (data[i - 1].NormalType == TypeNormal.NewLine) result[1] = i - 2;
                            if (data[result[1]].RoughType == TypeRough.Comment) result[1]--;
                            if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                            returnValue.Add(result);
                            break;
                        }
                        break;
                        #endregion
                    case TypeFine.Const:
                    case TypeFine.Define:
                    case TypeFine.Use:
                    case TypeFine.Var:
                        #region Const, Define, Use, Var
                        for (; i < Count; i++)
                        {
                            if (data[i].FineType != TypeFine.Semicolon) continue;
                            result[1] = i;
                            if (data[i].NormalType == TypeNormal.NewLine) result[1] = i - 1;
                            if (data[result[1]].RoughType == TypeRough.Comment) result[1]--;
                            if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                            returnValue.Add(result);
                            break;
                        }
                        break;
                        #endregion
                    case TypeFine.Function:
                    case TypeFine.Private:
                    case TypeFine.Protected:
                    case TypeFine.Public:
                    case TypeFine.Static:
                    case TypeFine.Namespace:
                        for (; i < Count; i++)
                        {
                            if (data[i].FineType != TypeFine.Semicolon && data[i].FineType != TypeFine.BlockOpenBracket) continue;
                            result[1] = data[i].FineType == TypeFine.Semicolon ? i : i - 1;
                            if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                            if (data[result[1]].RoughType == TypeRough.Comment) result[1]--;
                            if (data[result[1]].NormalType == TypeNormal.NewLine) result[1]--;
                            returnValue.Add(result);
                            break;
                        }
                        break;
                }
            }
            return returnValue;
        }

        #region GetNext(…)

        /// <summary>
        /// Returns the next Token with the defined rought type, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="type">The type</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeRough type, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (type != data[index].RoughType) continue;
                result = data[index];
                return true;
            }
            return false;
        }
        /// <summary>
        /// Returns the next Token with one of the defined rough types, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="types">The types</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeRough[] types, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (types.Any(type => type == data[index].RoughType))
                {
                    result = data[index];
                    return true;
                }
            }
            return false;
        }
        /// <summary>
        /// Returns the next Token with the defined normal type, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="type">The type</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeNormal type, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (type != data[index].NormalType) continue;
                result = data[index];
                return true;
            }
            return false;
        }
        /// <summary>
        /// Returns the next Token with one of the defined normal types, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="types">The types</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeNormal[] types, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (types.Any(type => type == data[index].NormalType))
                {
                    result = data[index];
                    return true;
                }
            }
            return false;
        }
        /// <summary>
        /// Returns the next Token with the defined fine type, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="type">The type</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeFine type, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (type != data[index].FineType) continue;
                result = data[index];
                return true;
            }
            return false;
        }
        /// <summary>
        /// Returns the next Token with one of the defined fine types, beginning search at token with the defined index.
        /// </summary>
        /// <param name="index">The token index where to start search</param>
        /// <param name="types">The types</param>
        /// <param name="result">Returns the resulting token, if method returns <c>true</c></param>
        /// <returns>bool</returns>
        public bool GetNext(int index, TypeFine[] types, out Token result)
        {
            result = null;
            for (int i = index; i < Count; i++)
            {
                if (types.Any(type => type == data[index].FineType))
                {
                    result = data[index];
                    return true;
                }
            }
            return false;
        }

        #endregion

        #endregion

    }

}